package jp.syuriken.snsw.terenaibot.format;

import java.util.ArrayList;
import java.util.TreeMap;

import jp.syuriken.snsw.terenaibot.BotConfiguration;
import jp.syuriken.snsw.terenaibot.DataProviderFactory;
import jp.syuriken.snsw.terenaibot.format.function.AndFunction;
import jp.syuriken.snsw.terenaibot.format.function.EqualFunction;
import jp.syuriken.snsw.terenaibot.format.function.IfEqFunction;
import jp.syuriken.snsw.terenaibot.format.function.IfFunction;
import jp.syuriken.snsw.terenaibot.format.function.IntAddFunction;
import jp.syuriken.snsw.terenaibot.format.function.IntDivideFunction;
import jp.syuriken.snsw.terenaibot.format.function.IntGreaterThanEqualFunction;
import jp.syuriken.snsw.terenaibot.format.function.IntGreaterThanFunction;
import jp.syuriken.snsw.terenaibot.format.function.IntLessThanEqualFunction;
import jp.syuriken.snsw.terenaibot.format.function.IntLessThanFunction;
import jp.syuriken.snsw.terenaibot.format.function.IntMultiFunction;
import jp.syuriken.snsw.terenaibot.format.function.IntRemainFunction;
import jp.syuriken.snsw.terenaibot.format.function.IntSubFunction;
import jp.syuriken.snsw.terenaibot.format.function.NotFunction;
import jp.syuriken.snsw.terenaibot.format.function.NullFunction;
import jp.syuriken.snsw.terenaibot.format.function.OrFunction;
import jp.syuriken.snsw.terenaibot.format.function.RandFunction;
import jp.syuriken.snsw.terenaibot.format.function.SetdataFunction;
import jp.syuriken.snsw.utils.Logger;

/**
 * ポストをフォーマットするためのクラスです。 
 * @author $Author: snswinhaiku.lo@gmail.com $
 */
public class PostFormatter {
	
	/**
	 * constプレフィックスプロパティー
	 * 
	 * @author $Author: snswinhaiku.lo@gmail.com $
	 */
	public class ConstPrefixProperty implements FormatPrefix {
		
		/** プロパティリスト */
		protected TreeMap<String, String> properties = makeProperties();
		

		/**
		 * 定数値をキーと関連付けて追加する
		 * 
		 * @param key 値に関連付けられたキー
		 * @param value 値
		 */
		public void addConst(String key, String value) {
			properties.put(key, value);
		}
		
		@Override
		public String getString(CompileArg compileArg, String name) {
			return properties.get(name);
		}
		
		private TreeMap<String, String> makeProperties() {
			TreeMap<String, String> map = new TreeMap<String, String>();
			map.put("dollar", "$");
			map.put("nbsp", " ");
			return map;
		}
		
	}
	
	private enum FormatStatus {
		DEFAULT, IN_FUNCTION, IN_PROPERTY, IN_PROPERTY_NAME, IN_PROPERTY_DEFAULT_VALUE
	}
	
	private class PostFormatCompileArg implements CompileArg {
		
		private final PostFormatter formatter;
		
		private final FormatPrefixHolder overridePrefixHolder;
		

		public PostFormatCompileArg(PostFormatter formatter, FormatPrefixHolder overridePrefixHolder) {
			this.formatter = formatter;
			this.overridePrefixHolder = overridePrefixHolder;
		}
		
		@Override
		public DataProviderFactory getDataProviderFactory() {
			return configuration.getDataProviderFactory();
		}
		
		@Override
		public FormatFunction getFormatFunction(String name) throws FormatSyntaxException {
			FormatFunction function = formatter.functionPool.get(name);
			if (function == null) {
				throw new FormatSyntaxException(String.format("Function %s is not found.", name));
			} else {
				return function;
			}
		}
		
		@Override
		public Logger getLogger() {
			return configuration.getLogger();
		}
		
		@Override
		public String getProperty(String name) {
			return getProperty("const", name);
		}
		
		@Override
		public String getProperty(String prefix, String name) {
			return getProperty(prefix, name, "");
		}
		
		@Override
		public String getProperty(String prefix, String name, String defaultValue) {
			String string = null;
			if (overridePrefixHolder.containsKey(prefix)) {
				string = overridePrefixHolder.get(prefix).getString(this, name);
			} else if (formatter.prefixHolder.containsKey(prefix)) {
				string = formatter.prefixHolder.get(prefix).getString(this, name);
			}
			if (string == null) {
				string = defaultValue;
			}
			return string;
		}
		
		@Override
		public boolean parseBoolean(String string) {
			return !(string == null || string.isEmpty() || string.equalsIgnoreCase("false") || string
				.equalsIgnoreCase("no"));
		}
	}
	

	/** 標準関数リスト */
	protected static TreeMap<String, FormatFunction> stdlibFunctions = generateStdLib();
	

	private static TreeMap<String, FormatFunction> generateStdLib() {
		TreeMap<String, FormatFunction> stdlibFunctionsMap = new TreeMap<String, FormatFunction>();
		stdlibFunctionsMap.put("ifeq", new IfEqFunction());
		stdlibFunctionsMap.put("if", new IfFunction());
		stdlibFunctionsMap.put("setdata", new SetdataFunction());
		stdlibFunctionsMap.put("equal", new EqualFunction());
		stdlibFunctionsMap.put("not", new NotFunction());
		stdlibFunctionsMap.put("and", new AndFunction());
		stdlibFunctionsMap.put("or", new OrFunction());
		stdlibFunctionsMap.put("+", new IntAddFunction());
		stdlibFunctionsMap.put("-", new IntSubFunction());
		stdlibFunctionsMap.put("*", new IntMultiFunction());
		stdlibFunctionsMap.put("/", new IntDivideFunction());
		stdlibFunctionsMap.put("%", new IntRemainFunction());
		stdlibFunctionsMap.put("mod", new IntRemainFunction());
		stdlibFunctionsMap.put("lt", new IntLessThanFunction());
		stdlibFunctionsMap.put("gt", new IntGreaterThanFunction());
		stdlibFunctionsMap.put("lte", new IntLessThanEqualFunction());
		stdlibFunctionsMap.put("gte", new IntGreaterThanEqualFunction());
		stdlibFunctionsMap.put("rand", new RandFunction());
		stdlibFunctionsMap.put("null", new NullFunction());
		return stdlibFunctionsMap;
	}
	

	private PostFormatter parent;
	
	private FormatPrefixHolder prefixHolder;
	
	private int precompiledIndex;
	
	private int precompilingLength;
	
	private BotConfiguration configuration;
	
	/** 関数プール */
	public TreeMap<String, FormatFunction> functionPool;
	

	/**
	 * インスタンスを生成する。
	 * 
	 * @param configuration ボット設定
	 */
	public PostFormatter(BotConfiguration configuration) {
		this.configuration = configuration;
		
		functionPool = new TreeMap<String, FormatFunction>(stdlibFunctions);
		prefixHolder = new FormatPrefixHolder();
		prefixHolder.put("const", new ConstPrefixProperty());
	}
	
	/**
	 * 親を引き継いでインスタンスを生成する。
	 * 
	 * <p>親の関数プールなどを変更すると生成したインスタンスでも変更されます。</p>
	 * @param configuration ボット設定
	 * @param parent 親PostFormatter
	 */
	public PostFormatter(BotConfiguration configuration, PostFormatter parent) {
		this.configuration = configuration;
		this.parent = parent;
		
		functionPool = new TreeMap<String, FormatFunction>(parent.functionPool);
		prefixHolder = new FormatPrefixHolder(parent.prefixHolder);
		
	}
	
	/**
	 * 定数文字列を作成する
	 * @param trim StringBuilderの値をtrim()するかどうか
	 * @param sb StringBuilder
	 * @param precompileSyntaxes PrecompiledSyntaxを格納するArrayList
	 */
	private void addConstSyntax(boolean trim, StringBuilder sb, ArrayList<PrecompiledSyntax> precompileSyntaxes) {
		if (sb.length() != 0) {
			String string = sb.toString();
			if (trim) {
				string = string.trim();
			}
			precompileSyntaxes.add(new ConstSyntax(string));
			sb.setLength(0);
		}
	}
	
	/**
	 * {@link FormatPrefix} を追加する。
	 * 
	 * @param prefixName プレフィックス名
	 * @param formatPrefix FormatPrefixサブクラスインスタンス
	 * @return プレフィックス名に以前に関連付けられていたインスタンス。存在しなかった場合は null。
	 */
	public FormatPrefix addFormatPrefix(String prefixName, FormatPrefix formatPrefix) {
		return prefixHolder.put(prefixName, formatPrefix);
	}
	
	/**
	 * {@link FormatFunction} を追加する。
	 * 
	 * @param name 関数名
	 * @param function FormatFunctionサブクラスインスタンス
	 * @return 関数名に以前に関連付けられていたインスタンス。存在しなかった場合は null。
	 */
	public FormatFunction addFunction(String name, FormatFunction function) {
		return functionPool.put(name, function);
	}
	
	/**
	 * プリコンパイル済みのデータをフォーマットする。
	 * 
	 * @param precompiledSyntax プリコンパイル済みのデータ
	 * @return フォーマット済み文字列
	 * @throws FormatSyntaxException フォーマット中にエラーが発生しました。
	 */
	public String format(PrecompiledSyntax precompiledSyntax) throws FormatSyntaxException {
		return format(precompiledSyntax, new FormatPrefixHolder());
	}
	
	/**
	 * プリコンパイル済みのデータをフォーマットする。
	 * 
	 * @param precompiledSyntax プリコンパイル済みのデータ
	 * @param prefix 優先されるプレフィックスを格納したクラス。
	 * @return フォーマット済み文字列
	 * @throws FormatSyntaxException フォーマット中にエラーが発生しました。
	 */
	public String format(PrecompiledSyntax precompiledSyntax, FormatPrefixHolder prefix) throws FormatSyntaxException {
		CompileArg compileArg = new PostFormatCompileArg(this, prefix);
		return precompiledSyntax.format(compileArg);
	}
	
	/**
	 * 親を取得する。
	 * 
	 * @return 親
	 */
	public PostFormatter getParent() {
		return parent;
	}
	
	/**
	 * 指定されたプレフィックスに対応するFormatPrefixを返します。
	 * 変更すると内部でも適用されます。
	 * @param prefix プレフィックス
	 * @return フォーマットプレフィックス
	 */
	public FormatPrefix getPrefix(String prefix) {
		return prefixHolder.get(prefix);
	}
	
	/**
	 * $ではじまる構文を処理する。
	 * 
	 * @param charArray 処理するchar[]。indexはprecompiledIndexが使われます。
	 * @param sb StringBuilder
	 * @param precompileSyntaxes プリコンパイル済みのデータを格納するArrayList
	 * @param nowchar 今処理している文字
	 * @throws FormatSyntaxException プリコンパイル中にエラーが発生しました。
	 */
	private void handleDollar(char[] charArray, StringBuilder sb, ArrayList<PrecompiledSyntax> precompileSyntaxes,
			char nowchar) throws FormatSyntaxException {
		switch (nowchar) {
			case '$': // "$$"
				sb.append('$');
				break;
			case '{': // "${"
				addConstSyntax(false, sb, precompileSyntaxes);
				precompileSyntaxes.add(precompile(charArray, FormatStatus.IN_PROPERTY));
				break;
			case '(': // "$("
				addConstSyntax(false, sb, precompileSyntaxes);
				precompileSyntaxes.add(precompile(charArray, FormatStatus.IN_FUNCTION));
				break;
			case '}': // "$}"
			case ')': // "$)"
			case ':': // "$:"
			case '=': // "$="
			case '|': // "$|"
				sb.append(nowchar);
				break;
			default:
		}
	}
	
	private void nextTokenPrecompiling(StringBuilder sb, ArrayList<PrecompiledSyntax> precompileSyntaxes,
			ArrayList<PrecompiledSyntaxGroup> syntaxGroups) {
		addConstSyntax(true, sb, precompileSyntaxes);
		syntaxGroups.add(new PrecompiledSyntaxGroup(precompileSyntaxes.toArray(new PrecompiledSyntax[precompileSyntaxes
			.size()])));
		precompileSyntaxes.clear();
	}
	
	private synchronized PrecompiledSyntax precompile(char[] charArray, FormatStatus startStatus)
			throws FormatSyntaxException {
		StringBuilder sb = new StringBuilder();
		ArrayList<PrecompiledSyntax> precompileSyntaxes = new ArrayList<PrecompiledSyntax>();
		ArrayList<PrecompiledSyntaxGroup> syntaxGroups = new ArrayList<PrecompiledSyntaxGroup>();
		FormatStatus status = startStatus;
		boolean previousDollar = false;
		while (precompiledIndex < precompilingLength) {
			char nowchar = charArray[precompiledIndex++];
			if (previousDollar) {
				handleDollar(charArray, sb, precompileSyntaxes, nowchar);
				previousDollar = false;
				continue;
			}
			switch (status) {
				case DEFAULT:
					switch (nowchar) {
						case '$':
							previousDollar = true;
							break;
						default:
							sb.append(nowchar);
					}
					break;
				case IN_FUNCTION:
					switch (nowchar) {
						case '$':
							previousDollar = true;
							break;
						case '|':
							nextTokenPrecompiling(sb, precompileSyntaxes, syntaxGroups);
							break;
						case ')':
							addConstSyntax(true, sb, precompileSyntaxes);
							syntaxGroups.add(new PrecompiledSyntaxGroup(precompileSyntaxes
								.toArray(new PrecompiledSyntax[precompileSyntaxes.size()])));
							return new PrecompiledFunction(syntaxGroups);
						default:
							sb.append(nowchar);
							break;
					}
					break;
				case IN_PROPERTY:
					switch (nowchar) {
						case ':':
							nextTokenPrecompiling(sb, precompileSyntaxes, syntaxGroups);
							status = FormatStatus.IN_PROPERTY_NAME;
							break;
						case '=':
							nextTokenPrecompiling(sb, precompileSyntaxes, syntaxGroups);
							status = FormatStatus.IN_PROPERTY_DEFAULT_VALUE;
							break;
						case '}':
							precompileSyntaxes.add(new ConstSyntax(sb.toString()));
							syntaxGroups.add(new PrecompiledSyntaxGroup(precompileSyntaxes
								.toArray(new PrecompiledSyntax[precompileSyntaxes.size()])));
							return new PrecompiledProperty(syntaxGroups);
						default:
							sb.append(nowchar);
							break;
					}
					break;
				case IN_PROPERTY_NAME:
					switch (nowchar) {
						case '=':
							nextTokenPrecompiling(sb, precompileSyntaxes, syntaxGroups);
							status = FormatStatus.IN_PROPERTY_DEFAULT_VALUE;
							break;
						case '}':
							nextTokenPrecompiling(sb, precompileSyntaxes, syntaxGroups);
							return new PrecompiledProperty(syntaxGroups);
						default:
							sb.append(nowchar);
							break;
					}
					break;
				case IN_PROPERTY_DEFAULT_VALUE:
					switch (nowchar) {
						case '}':
							nextTokenPrecompiling(sb, precompileSyntaxes, syntaxGroups);
							return new PrecompiledProperty(syntaxGroups);
						default:
							sb.append(nowchar);
					}
					break;
				default:
					sb.append('$');
					sb.append(nowchar);
					break;
			}
		}
		precompileSyntaxes.add(new ConstSyntax(sb.toString()));
		if (startStatus == FormatStatus.DEFAULT) {
			return new PrecompiledSyntaxGroup(precompileSyntaxes.toArray(new PrecompiledSyntax[precompileSyntaxes
				.size()]));
		} else if (startStatus == FormatStatus.IN_FUNCTION) {
			throw new FormatSyntaxException("Expected ')' but got EOL");
		} else {
			throw new FormatSyntaxException("Expected '}' but got EOL");
		}
	}
	
	/**
	 * 指定された文字列を使用して解析を行います。解析済みのシンタックスは
	 * {@link PostFormatter#format(PrecompiledSyntax)}などを利用してフォーマットを行って下さい
	 * 
	 * @param string 解析する文字列
	 * @return 解析済みのシンタックス
	 * @throws FormatSyntaxException 正しくない形式の文字列です
	 */
	public synchronized PrecompiledSyntax precompile(String string) throws FormatSyntaxException {
		precompiledIndex = 0;
		precompilingLength = string.length();
		return precompile(string.toCharArray(), FormatStatus.DEFAULT);
	}
}
